package com.googlecode.androidkurskoblenz.cub3snake;

import java.util.LinkedList;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.os.Handler;

public class Game
{
	private class FinishedException extends Exception
	{
		private static final long serialVersionUID = -7404108590525615328L;
	}
	
	public class Cell
	{
		private CellState state;
		
		private byte x, y, z;
		
		private Cell(CellState state, byte x, byte y, byte z)
		{
			this.state = state;
			this.x = x;
			this.y = y;
			this.z = z;
		}
		
		public CellState getState()
		{
			return state;
		}
		
		public void setState(CellState state)
		{
			this.state = state;
			controller.onGameCellChange(x, y, z, state);
		}
	}
	
	public enum CellState
	{
		CLEAR, FOOD, SNAKE
	}
	
	public enum Direction
	{
		BACKWARD, DOWN, FORWARD, LEFT, RIGHT, UP
	}
	
	public enum State
	{
		PAUSED, RUNNING, STOPPED
	}
	
	private static final double BASE_SLOPE = 1.1;
	private static final int BASE_TIME_DIFFICULT = 500;
	private static final int BASE_TIME_EASY = 1500;
	private static final int BASE_TIME_MEDIUM = 1000;
	private static final byte LENGTH_SNAKE = 2;
	
	private int base_time = BASE_TIME_MEDIUM;
	private Cell cells[][][];
	private GameInterface controller;
	private Direction direction = Direction.RIGHT;
	private Handler handler = new Handler();
	private LinkedList<Cell> snake = new LinkedList<Cell>();
	private State state;
	private Runnable stepper = new Runnable()
	{
		@Override
		public void run()
		{
			Game.this.step();
		}
	};
	private byte width, depth, height;
	
	/*
	 * creates an empty Cube model with the specified dimensions
	 */
	Game(GameInterface controller, byte width, byte depth, byte height)
	{
		this.controller = controller;
		
		this.width = width;
		this.depth = depth;
		this.height = height;
		
		cells = new Cell[height][depth][width];
		for(byte i = 0; i < width; i++)
			for(byte j = 0; j < depth; j++)
				for(byte k = 0; k < height; k++)
					cells[k][j][i] = new Cell(CellState.CLEAR, i, j, k);
		
		for(byte i = 0; i < LENGTH_SNAKE; i++)
		{
			Cell cell = cells[0][0][i];
			this.snake.add(0, cell);
			cell.setState(CellState.SNAKE);
		}
		
		try
		{
			generateFood();
		}
		catch(FinishedException e)
		{
			setState(State.STOPPED);
			controller.onGameStop();
			return;
		}
		
		setState(State.PAUSED);
	}
	
	/*
	 * creates a new Cube model and fills it according to <str>
	 * @see Game.serialize() for the serialization
	 */
	Game(GameInterface controller, String str) throws UnsupportedOperationException
	{
		this.controller = controller;
		
		try
		{
			JSONObject data = new JSONObject(str);
			
			width = (byte) data.getInt("width");
			depth = (byte) data.getInt("depth");
			height = (byte) data.getInt("height");
			
			cells = new Cell[height][depth][width];
			for(byte i = 0; i < width; i++)
				for(byte j = 0; j < depth; j++)
					for(byte k = 0; k < height; k++)
						cells[k][j][i] = new Cell(CellState.CLEAR, i, j, k);
			
			byte x, y, z;
			JSONArray snake = data.getJSONArray("snake");
			int parts = (int) Math.floor(snake.length() / 3);
			if(parts < 1)
				throw new UnsupportedOperationException();
			
			for(int i = 0; i < parts; i++)
			{
				x = ((Integer) snake.get(i * 3 + 0)).byteValue();
				y = ((Integer) snake.get(i * 3 + 1)).byteValue();
				z = ((Integer) snake.get(i * 3 + 2)).byteValue();
				
				Cell cell = cells[z][y][x];
				this.snake.add(cell);
				cell.setState(CellState.SNAKE);
			}
			
			direction = Game.Direction.values()[data.getInt("direction")];
			
			x = (byte) data.getInt("food_x");
			y = (byte) data.getInt("food_y");
			z = (byte) data.getInt("food_z");
			
			Cell cell = cells[z][y][x];
			cell.setState(CellState.FOOD);
			
			setState(State.PAUSED);
		}
		catch(JSONException e)
		{
			throw new UnsupportedOperationException("Invalid JSON data", e);
		}
	}
	
	private void generateFood() throws FinishedException
	{
		byte x, y, z;
		Cell target;
		
		if(snake.size() == width * height * depth)
			// pretty sure this won't ever happen...
			throw new FinishedException();
		
		while(true)
		{
			x = (byte) (Math.random() * width);
			y = (byte) (Math.random() * depth);
			z = (byte) (Math.random() * height);
			
			target = cells[z][y][x];
			if(target.state != CellState.CLEAR)
				continue;
			target.setState(CellState.FOOD);
			break;
		}
	}
	
	private void setState(State state)
	{
		this.state = state;
		this.controller.onGameStateChange(state);
	}
	
	private void step()
	{
		Cell head = snake.getFirst();
		byte x = head.x;
		byte y = head.y;
		byte z = head.z;
		
		boolean failed = false;
		
		switch(direction)
		{
			case LEFT:
				if(x == 0)
					failed = true;
				else
					x--;
				break;
			
			case RIGHT:
				if(x >= width - 1)
					failed = true;
				else
					x++;
				break;
			
			case BACKWARD:
				if(y == 0)
					failed = true;
				else
					y--;
				break;
			
			case FORWARD:
				if(y >= depth - 1)
					failed = true;
				else
					y++;
				break;
			
			case DOWN:
				if(z == 0)
					failed = true;
				else
					z--;
				break;
			
			case UP:
				if(z >= height - 1)
					failed = true;
				else
					z++;
				break;
		}
		
		Cell target = null;
		
		if(!failed)
		{
			target = cells[z][y][x];
			
			if(target.state == CellState.SNAKE)
				failed = true;
		}
		
		if(!failed)
		{
			boolean food = true;
			
			if(target.state != CellState.FOOD)
			{
				Cell end = snake.removeLast();
				end.setState(CellState.CLEAR);
				food = false;
			}
			else
			{
				try
				{
					generateFood();
				}
				catch(FinishedException e)
				{
					setState(State.STOPPED);
					controller.onGameStop();
					return;
				}
			}
			
			target.setState(CellState.SNAKE);
			snake.add(0, target);
			
			controller.onGameStep(food);
			
			handler.postDelayed(stepper, (long) (base_time * Math.pow(BASE_SLOPE, getPoints())));
		}
		else
		{
			setState(State.STOPPED);
			controller.onGameStop();
		}
	}
	
	public CellState getCellState(int x, int y, int z)
	{
		return this.cells[z][y][x].getState();
	}
	
	public byte getDepth()
	{
		return depth;
	}
	
	public byte getHeight()
	{
		return height;
	}
	
	public int getPoints()
	{
		return snake.size() - LENGTH_SNAKE;
	}
	
	public State getState()
	{
		return state;
	}
	
	public byte getWidth()
	{
		return width;
	}
	
	public void pause() throws Exception
	{
		if(state != State.RUNNING)
			throw new Exception();
		
		setState(State.PAUSED);
		handler.removeCallbacks(stepper);
	}
	
	public String serialize() throws UnsupportedOperationException
	{
		try
		{
			JSONObject data = new JSONObject();
			
			data.put("width", width);
			data.put("depth", depth);
			data.put("height", height);
			
			JSONArray snake = new JSONArray();
			for(int i = 0; i < this.snake.size(); i++)
			{
				snake.put(this.snake.get(i).x);
				snake.put(this.snake.get(i).y);
				snake.put(this.snake.get(i).z);
			}
			data.put("snake", snake);
			
			data.put("direction", direction.ordinal());
			
			Cell cell;
			food:
			for(byte x = 0; x < width; x++)
			{
				for(byte y = 0; y < depth; y++)
				{
					for(byte z = 0; z < height; z++)
					{
						cell = this.cells[z][y][x];
						if(cell.state == CellState.FOOD)
						{
							data.put("food_x", cell.x);
							data.put("food_y", cell.y);
							data.put("food_z", cell.z);
							break food;
						}
					}
				}
			}
			
			return data.toString();
		}
		catch(JSONException e)
		{
			throw new UnsupportedOperationException(e);
		}
	}
	
	public void setDifficulty(int difficulty)
	{
		switch(difficulty)
		{
			case 0:
				base_time = BASE_TIME_EASY;
				break;
			case 1:
				base_time = BASE_TIME_MEDIUM;
				break;
			case 2:
				base_time = BASE_TIME_DIFFICULT;
				break;
		}
	}
	
	public void setDirection(Direction direction)
	{
		this.direction = direction;
	}
	
	public void start() throws Exception
	{
		if(state != State.PAUSED)
			throw new Exception();
		
		setState(State.RUNNING);
		handler.postDelayed(stepper, (long) (base_time * Math.pow(BASE_SLOPE, getPoints())));
	}
}
